/*
 * Copyright (C) National Biorepository Project, KFSHRC, 2003
 * Created on Aug 19, 2003 by aaa
 *
 */
package sa.edu.kfshrc.nb.server.actions.beans;

import java.rmi.RemoteException;
import java.util.Collection;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.Set;
import java.util.TreeMap;
import java.util.Vector;
import java.util.logging.Logger;

import javax.ejb.EJBException;
import javax.ejb.FinderException;
import javax.ejb.SessionContext;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.transaction.UserTransaction;

import sa.edu.kfshrc.nb.server.Configurator;
import sa.edu.kfshrc.nb.server.actions.support.ActionRequest;
import sa.edu.kfshrc.nb.server.actions.support.LoginToken;
import sa.edu.kfshrc.nb.server.database.PrimaryKey;
import sa.edu.kfshrc.nb.server.entities.beans.ActionBean;
import sa.edu.kfshrc.nb.server.entities.beans.PermissionBean;
import sa.edu.kfshrc.nb.server.entities.beans.UserBean;
import sa.edu.kfshrc.nb.server.entities.interfaces.PermissionLocal;
import sa.edu.kfshrc.nb.server.entities.interfaces.PermissionLocalHome;
import sa.edu.kfshrc.nb.server.entities.interfaces.RoleLocal;
import sa.edu.kfshrc.nb.server.entities.interfaces.RoleRemote;
import sa.edu.kfshrc.nb.server.entities.interfaces.RoleUserMappingLocal;
import sa.edu.kfshrc.nb.server.entities.interfaces.RoleUserMappingLocalHome;
import sa.edu.kfshrc.nb.server.entities.interfaces.RoleUserMappingRemote;
import sa.edu.kfshrc.nb.server.entities.interfaces.UserLocal;
import sa.edu.kfshrc.nb.server.entities.interfaces.UserLocalHome;
import sa.edu.kfshrc.nb.server.entities.support.PermissionLocalComparator;
import sa.edu.kfshrc.nb.server.entities.support.PropertyConstraint;
import sa.edu.kfshrc.nb.server.exceptions.NBServerException;
import sa.edu.kfshrc.nb.server.logging.NBLevel;

/**
 * @author aaa
 * Created on Aug 26, 2003 at 11:59:34 PM.
 *
 * This the base class that should be extended by any other session bean in the
 * project. It is designed to reduce the amount of coding involved for the beans.
 * This is done by placing a number of helper functions in this class that
 * its derived children can refer to. There are also some private data members
 * that are put here because they are common to all session beans in this
 * project.
 */
public abstract class BaseSessionBean
{
	// ----- Static members ---------------------------------------------------
	// potential checks to do pre action execution
	protected static final long CHK_LOGGED_IN = 0x1;
	protected static final long CHK_RECOGNIZED_ACTION = 0x2;
	protected static final long CHK_AUTHORIZED_FOR_ACTION = 0x4;

	// Indicators of a user's logged in status 
	public final static short USER_LOGGED_IN = 0;
	public final static short USER_INVALID_TOKEN = 1;
	public final static short USER_BLOCKED = 2;
	public final static short USER_NONEXISTENT = 3;

	// ----- Instance members -------------------------------------------------
	protected SessionContext context;

	/**
	 * 
	 */
	public BaseSessionBean()
	{
		(Configurator.getMainLogger()).log(NBLevel.NB_TRACE, "Entered constructor");
	}

	// ----- EJB required methods ---------------------------------------------
	public void ejbActivate() throws EJBException
	{
		(Configurator.getMainLogger()).log(NBLevel.NB_TRACE, 
			"Entered ejbActivate on behalf of " + this.getClass().getName());
	}

	public void ejbPassivate() throws EJBException
	{
		(Configurator.getMainLogger()).log(NBLevel.NB_TRACE, 
			"Entered ejbPassivate on behalf of " + this.getClass().getName());
	}

	public void ejbRemove() throws EJBException, RemoteException
	{
		(Configurator.getMainLogger()).log(NBLevel.NB_TRACE, 
			"Entered ejbRemove on behalf of " + this.getClass().getName());
	}

	public void setSessionContext(SessionContext input_context)
		throws EJBException, RemoteException
	{
		(Configurator.getMainLogger()).log(NBLevel.NB_TRACE, 
			"Entered setSessionContext on behalf of " + this.getClass().getName());
		this.context = input_context;
	}

	// ----- Business logic methods -------------------------------------------
	protected boolean isSystemBlocked()
	{
		// TODO: add code to check against system block
		return false;		
	}

	protected short checkLoggedInUser(LoginToken loginToken)
		throws NBServerException
	{
		try
		{
			if (loginToken == null)
				return BaseSessionBean.USER_NONEXISTENT;

			// check if token is valid, including whether it has expired
			if (!loginToken.isValid())
				return BaseSessionBean.USER_INVALID_TOKEN;

			// second: check if that user exists and is not currently blocked
			PrimaryKey userID = new PrimaryKey(loginToken.getUserID());
			Context ctx = new InitialContext();
			Object obj = ctx.lookup("java:comp/env/ejb/UserLocalHome");
			UserLocalHome userHome = (UserLocalHome) obj;
		
			UserLocal user = userHome.findByPrimaryKey(userID); 
			if (user == null)
				return BaseSessionBean.USER_NONEXISTENT;

			if ((user.getStatus() & UserBean.ST_BLOCKED) > 0)
				return BaseSessionBean.USER_BLOCKED;

			return BaseSessionBean.USER_LOGGED_IN;
		}
		catch (Exception e)
		{
			(Configurator.getMainLogger()).log(NBLevel.NB_DEBUG, 
				"Unable to check if a user is logged in or not.", e);
			throw new NBServerException("error.UnableToCheckLogin", true, e);
		}
	}

	protected boolean isRecognizedAction(String actionName, short actionVersion)
	{
		// Look up the action by its name and version number
		// If found, fine. Else, not.
		UserTransaction userTran = null;
		Logger log = Configurator.getMainLogger();
		Collection actions;

		try
		{
			if (actionName == null || actionName.equals(""))
				return false;

			Hashtable allActions = ActionBean.getSnapshotOfCachedTable();
			
			System.out.println("Number of actions: " + allActions.size()); // TODO: rm
			
			Enumeration enumeration = allActions.elements();
			while (enumeration.hasMoreElements())
			{
				Hashtable record = (Hashtable) enumeration.nextElement();
				if (actionName.equalsIgnoreCase((String) record.get("englishname")) ||
					actionName.equalsIgnoreCase((String) record.get("arabicname")))
				{
					if (actionVersion == ((Short) record.get("actionversion")).shortValue())
						return true; 
				}				
			}

			/* Another way to do the same without making use of cache...if you use it,
			 * make sure to uncomment the code in the catch statement as well...
			Context ctx = new InitialContext();
			
			Object obj = ctx.lookup("java:comp/env/ejb/ActionLocalHome");
			ActionLocalHome actionHome = (ActionLocalHome) obj;

			userTran = context.getUserTransaction();			
			userTran.begin();

			actions = actionHome.findByEnglishNameVersion(actionName, actionVersion);

			if (!actions.isEmpty())
			{
				userTran.commit();
				return true;
			}
				
			userTran.commit();
			*/
			System.out.println("DID NOT FIND THE ACTION " + actionName + " " + actionVersion); // TODO
			return false;
		}
		catch (Exception e)
		{
			System.out.println("ERROR looking up action."); // TODO: rm
			e.printStackTrace();
			/*
			if (userTran != null)
			{
				log.log(NBLevel.NB_TRACE, "database.Rollback", e);
				try 
				{ 
					userTran.rollback(); 
				}
				catch (Exception e2) 
				{
					log.log(NBLevel.NB_ERROR, "database.UnableToRollback", e2);
				}
			}
			if (e.getClass().equals(FinderException.class))
				return false;
			*/
			(Configurator.getMainLogger()).log(NBLevel.NB_TRACE, "Could not determine if action is recognized", e);
			return false;
			// throw new NBServerException(e.toString(), false);
		}
	}
	
	protected boolean isAuthorized(PrimaryKey assigneeID, String actionName, short actionVersion,
		String objectTableName, PrimaryKey objectID, String objectGroup, Hashtable objectParameters)
		throws NBServerException
	{
		UserTransaction userTran = null;
		Logger log = Configurator.getMainLogger();
		Collection permissions, assigneeRoleUserMappings;
		Vector assigneeRoles = new Vector();

		try
		{
			Context ctx = new InitialContext();

			Object obj = ctx.lookup("java:comp/env/ejb/PermissionLocalHome");
			PermissionLocalHome permissionHome = (PermissionLocalHome) obj;
			Object obj2 = ctx.lookup("java:comp/env/ejb/RoleUserMappingLocalHome");
			RoleUserMappingLocalHome rumHome = (RoleUserMappingLocalHome) obj2;

			userTran = context.getUserTransaction();			
			userTran.begin();

			assigneeRoleUserMappings = rumHome.findByUserID(assigneeID);
			
			if (assigneeRoleUserMappings.isEmpty())
				throw new NBServerException("error.UserHasNoRoles", true);
			else
			{
				Iterator mappingsIterator = assigneeRoleUserMappings.iterator();
				
				while (mappingsIterator.hasNext())
				{
					RoleUserMappingLocal ruml = (RoleUserMappingLocal) mappingsIterator.next();
					assigneeRoles.add(ruml.getRoleID());
				}
				
			}

			if (Configurator.getDebugLevel() == 10)
			{
				System.out.println("Attempting to find matching permissions for: ");
				System.out.println("Assignee: " + assigneeID.toString());
				System.out.println("Action name: " + actionName);
				System.out.println("Action version: " + actionVersion);
				System.out.println("Object table: " + objectTableName);
				if (objectID != null)
					System.out.println("Object ID: " + objectID.toString());
				else
					System.out.println("Object ID: null");
				System.out.println("Object group: " + objectGroup);
				Iterator mappingsIterator = assigneeRoleUserMappings.iterator();
				while (mappingsIterator.hasNext())
					System.out.println("Assignee role: " + 
					((RoleUserMappingLocal) mappingsIterator.next()).getRoleID().toString());
			}

			permissions = permissionHome.findByCoreFields(assigneeID, assigneeRoles, actionName,
				actionVersion, objectTableName, objectID, objectGroup); 

			if (Configurator.getDebugLevel() == 10)
			{
				if (permissions.isEmpty())
					System.out.println("Empty set of permissions when finding by core fields");
			}

			// Assume we have a set of records (each record being a particular permission
			// that matches the input parameters).  The order of precedence can be summarized
			// as being "most specific match wins".
			// The algorithm for checking the permissions embodies a certain method for
			// resolving any conflicts between the stored permissions. This algorithm is OPEN
			// TO DEBATE and may need to be changed. See the class PermissionBean for how
			// the priorityLevel of permissions is computed based on the permission's other
			// attributes.
			
			Iterator resultsIterator = permissions.iterator();

			// Use a treemap to sort the permissions in descending order (the comparator
			// is built to sort in descending order).
			// Note that the SQL query that is being used does have an "ORDER BY" clause in 
			// it, but this is not guaranteed to be supported within the EJB specification: 
			// we do not know the order of the *Collection* that the findByCoreFields method 
			// returns. Too bad, eh? 
			TreeMap tree = new TreeMap(new PermissionLocalComparator());

			while (resultsIterator.hasNext())
			{
				PermissionLocal permissionLocal = (PermissionLocal) resultsIterator.next();
				tree.put(permissionLocal, "irrelevant");
			}

			// If we found matching permissions, they should be ordered properly in the
			// tree. Take the last one (i.e. the closest match), and return its status.
			if (!tree.isEmpty())
			{
				if (Configurator.getDebugLevel() == 10)
				{
					Set keys = tree.keySet();
					Iterator iterator = keys.iterator();
					while (iterator.hasNext())
					{
						PermissionLocal permissionLocal = (PermissionLocal) iterator.next();
						System.out.println(" ---- Permission ---- ");
						System.out.println(" PriorityLevel: " + permissionLocal.getPriorityLevel());
						System.out.println(" ID: " + permissionLocal.getID().toString());
						System.out.println(" AssignerRankID: " + permissionLocal.getAssignerRankID().toString());
						System.out.println(" ObjectID: " + permissionLocal.getObjectID());
						System.out.println(" ObjectConstraint: " + permissionLocal.getObjectConstraint().toString());
						System.out.println(" Status: " + permissionLocal.getStatus());
					}
				}

				// prepare to store the priority level of the highest priority permission 
				// matching the passed in parameters...
				long highestPriorityLevel = -1; 

				System.out.println("1000"); // TODO: rm
				// in the following loop, we will first look for the first matching permission.
				// Matching in the sense that it matches the parameters passed into this function
				// (the actionName, objectID, etc.). Note that matching also includes the constraint
				// but since the constraint cannot be checked at the database level, we have to
				// include code to check the permission's constraint.
				// After we find our first matching permission, store that permission's priority
				// level - it becomes our 'reference' highestPriorityLevel.
				// After we have found our highestPriorityLevel, then as we loop through the
				// permissions, we check the current permission's priority level against the
				// reference. If it is not the same, then we exit - recall the tree is sorted.
				// If it is the same, then check if that permission is constrained - if so,
				// confirm that the object matches the constraint, otherwise ignore the permission.
				// If not constrained or if constrained with a matching object, then check that
				// permission if it is granted or denied, along with its ancestors if any.
				Set keys = tree.keySet();
				Iterator keysIterator = keys.iterator();
				System.out.println("1010"); // TODO: rm
				while (keysIterator.hasNext())
				{
					PermissionLocal possibleMatchingPermission = (PermissionLocal) keysIterator.next();
					PropertyConstraint objectConstraint = possibleMatchingPermission.getObjectConstraint();

					System.out.println("1020"); // TODO: rm
					if (highestPriorityLevel == -1) // we haven't found a single matching permission yet
					{					   
						System.out.println("1030"); // TODO: rm
						if (objectConstraint.isValid()) // constraint on permission is valid, so check it
						{
							// ...check it against the passed in hashtable of objectParameters
							if (objectConstraint.evaluate(objectParameters))
							{
								// if it evaluates true, then we found our first matching permission 
								highestPriorityLevel = possibleMatchingPermission.getPriorityLevel();
							}
						}
						else // constraint on the permission is empty or uninitialized...so it matches
						{
							highestPriorityLevel = possibleMatchingPermission.getPriorityLevel();
							System.out.println("1040 " + highestPriorityLevel); // TODO: rm
						}
					}
					
					// if after checking the current possible matching permission, we still
					// don't have a match, then go on to the next permission
					if (highestPriorityLevel == -1)
					{
						System.out.println("1050"); // TODO: rm
						continue;
					}

					// if the current permission in the set has a different (lower) priority from 
					// the highest matching priority, we can stop checking: all the remaining 
					// permissions are of lower priority (remember, the tree's sorted).
					if ( possibleMatchingPermission.getPriorityLevel() != highestPriorityLevel )
					{
						System.out.println("1060"); // TODO: rm
						break;
					}
					// if we reach here, then we have already found at least one permission that
					// matches, so we have a valid highestPriorityLevel. We now check the current
					// permission at hand: is it constrained? If yes, then does the object
					// passed in match that permission - if not then ignore that permission,
					// otherwise check that permission. If no, then check that permission likewise.
					if (!objectConstraint.isValid() ||
						(objectConstraint.isValid() && objectConstraint.evaluate(objectParameters)))
					{
						System.out.println("1070"); // TODO: rm
						if( possibleMatchingPermission.getStatus() == PermissionBean.P_GRANTED )
						{
							// need to check the closest match's ancestor(s) permission (if any)
							// (will use the variable closestMatch to traverse the chain of ancestors here)
							while ( possibleMatchingPermission.getDependsUponID().isMatchingOne() )
							{
								System.out.println("1080"); // TODO: rm
								PermissionLocal ancestorPermission = 
									permissionHome.findByPrimaryKey(possibleMatchingPermission.getDependsUponID());
							
								// If a single ancestor represents a non-granted permission, then
								// deny the request.
								if (ancestorPermission.getStatus() != PermissionBean.P_GRANTED)
								{
									System.out.println("1090"); // TODO: rm
									userTran.commit();
									return false;
								}
	
								// Proceed to check the ancestor's ancestor...
								possibleMatchingPermission = ancestorPermission;
							}
						}
						else
						{
							System.out.println("1100"); // TODO: rm
							userTran.commit();
							return false;
						}
					}

				}
				
				if (highestPriorityLevel == -1) // we never found a single matching permission
				{
					System.out.println("1110"); // TODO: rm
					userTran.commit();
					return false;
				}

				// If we reach this stage, then we had a non-empty tree of permissions,
				// and the relevant ones (those that matched at a particular shared priority level)
				// and their ancestors all signified GRANTED as their status.
				userTran.commit();
				System.out.println("1120"); // TODO: rm
				return true;
			}
			else
			{
				if (Configurator.getDebugLevel() == 10)
				{
					System.out.println("Empty tree of permissions...");				
				}
			}
			userTran.commit();
			return false;
		}
		catch (Exception e)
		{
			if (userTran != null)
			{
				log.log(NBLevel.NB_TRACE, "database.Rollback", e);
				try 
				{ 
					userTran.rollback(); 
				}
				catch (Exception e2) 
				{
					log.log(NBLevel.NB_ERROR, "database.UnableToRollback", e2);
				}
			}
			if (e.getClass().equals(FinderException.class)) // i.e. no permission found.
				return false; // this is a 'normal' result, so don't throw the exception up

			(Configurator.getMainLogger()).log(NBLevel.NB_ERROR, "Could not determine authorization", e);
			if (e.getClass().equals(NBServerException.class))
				throw (NBServerException) e;

			throw new NBServerException("error.UnknownPermissionLookupError", true);
		}
	}

	/**
	 * This method should always be called from the derived session beans prior to actually executing any
	 * action. Depending on what the method returns, the action should either proceed or be halted.
	 * 
	 * @param actionName the action name (required, filled in typically by the session bean) - it is
	 * used to check that the user is calling the right function (will only be useful for future non-Web
	 * based interfaces inshaa-Allaah)
	 * @param actionRequest action request class (required)
	 * @param objectTableName the table name of the object to be acted on (optional)
	 * @param objectID the ID of the object to be acted on (optional)
	 * @param objectGroup the group of objects to be acted on (optional)
	 * @param objectParameters parameters of the object that may be used when checking against
	 * constraints in the permissions table (optional)
	 * 
	 * @return a String indicating the status of the check - null indicates success.
	 * @throws NBServerException
	 */
	protected String checkBeforeExecuteAction(String actionName, ActionRequest actionRequest, 
		String objectTableName, PrimaryKey objectID, String objectGroup, Hashtable objectParameters)
		throws NBServerException
	{
		try
		{
			if (actionRequest == null)
				return "error.NullInvalidActionRequest";
			
			if (isSystemBlocked())
				return "error.SystemBlocked";
			
			LoginToken loginToken = new LoginToken();
			loginToken.parse(actionRequest.getLoginToken());
			short loginStatus = checkLoggedInUser(loginToken);
			switch (loginStatus)
			{
				case BaseSessionBean.USER_INVALID_TOKEN:
					return "error.InvalidLoginToken";
				case BaseSessionBean.USER_BLOCKED:
					return "error.UserBlocked";
				case BaseSessionBean.USER_NONEXISTENT:
					return "error.UserNonexistent";
				case BaseSessionBean.USER_LOGGED_IN:
					break;
				default:
					return "error.UnableToCheckLogin";
			}
			
			System.out.println("1500 checkBeforeExecuteAction " + actionName + " "); // TODO: rm
			if (!actionName.equals(actionRequest.getActionName()) ||
				!isRecognizedAction(actionRequest.getActionName(), actionRequest.getActionVersion()))
				return "error.UnrecognizedAction";
			
			System.out.println("1600 checkBeforeExecuteAction " + actionName + " " + actionRequest.getUserID()); // TODO: rm
			
			if (!isAuthorized(new PrimaryKey(actionRequest.getUserID()), actionRequest.getActionName(),
				actionRequest.getActionVersion(), objectTableName, objectID, objectGroup, objectParameters))
				return "error.UserUnauthorized";
			System.out.println("1700 checkBeforeExecuteAction " + actionName + " "); // TODO: rm
			
			return null;
		}
		catch (NBServerException e)
		{
			return e.getMessage(); // don't localize it yet.
		}
		catch (Exception e)
		{
			return "error.UnknownPreActionError";
		}
	}
	
	/**
	 * This method is a simplified form of the checkBeforeActionExecution method. It is specific 
	 * only to the action "authenticateUser" and other similar functions for non-logged in users: 
	 * the user still is not authenticated and so has no login token or user ID to authorize against. 
	 * 
	 * @param actionRequest action request class (required)

	 * @return a String indicating the status of the check - null indicates success.
	 * @throws NBServerException
	 */
	protected String checkBeforeExecuteActionForGuest(String actionName, ActionRequest actionRequest)
	{
		if (actionRequest == null)
			return "error.NullInvalidActionRequest";
		
		if (isSystemBlocked())
			return "error.SystemBlocked";

		if (!actionName.equals(actionRequest.getActionName()) ||
			!isRecognizedAction(actionRequest.getActionName(), actionRequest.getActionVersion()))
			return "error.UnrecognizedAction";

		return null;		
	}
	
/*
	protected boolean checkBeforeActionExecution(long checks, ActionRequest actionRequest,
		String objectTableName, PrimaryKey objectID, String objectGroup, Hashtable objectParameters)
		throws NBServerException
	{
		if (actionRequest == null ||
			((checks & CHK_LOGGED_IN) > 0 && !isLoggedIn(rh.getLoginToken())) ||
			((checks & CHK_RECOGNIZED_ACTION) > 0 && 
			  !isRecognizedAction(rh.getActionName(), rh.getActionVersion())) ||
			((checks & CHK_AUTHORIZED_FOR_ACTION) > 0 &&
			  !isAuthorized(new PrimaryKey(rh.getUserID()), rh.getActionName(), 
			  rh.getActionVersion(), objectTableName, objectID, objectGroup, objectParameters))
			)
		{
			return false;
		}
		return true;
	}
*/	
}
